package player;

/*
 * NetworkPlayer.java
 *
 * Version:
 *   $Id: NetworkPlayer.java,v 1.1 2002/10/22 21:12:53 se362 Exp $
 *
 * Revisions:
 *   $Log: NetworkPlayer.java,v $
 *   Revision 1.1  2002/10/22 21:12:53  se362
 *   Initial creation of case study
 *
 */

import game.Driver;

import java.net.*;
import java.io.*;
import javax.swing.JOptionPane;

import networking.NetworkMove;

import controller.Move;
import controller.Rules;

import java.awt.Color;

/**
 * This class inerits from player. It is involved in the network game. This
 * identifies that a network player is the second player.
 * 
 * @author
 */
public class NetworkPlayer extends Player {

	// The commands that can be send back and forth
	private static int RESIGN = 0;
	private static int DRAWOFFER = 1;
	private static int ACCEPTDRAW = 2;
	private static int REFUSEDRAW = 3;
	private static int ENDOFGAME = 4;
	private static int ROGER = 90;
	private static int RESEND = 98;
	private static int NETWORKPLAYER = 1;
	
    private static int PORTNUM = 1051; // The port number we will read from
    private static int TIMEOUT = 600000; // Set timeout when waiting for client to connect (in milliseconds)
    private URL host = null; // The host we'll connect to if we're a remote system
    private Socket clientSocket = null; // The socket used for communication
    private ServerSocket serverSocket = null; // The socket that listens for a connect request
    private ObjectOutputStream out = null; // Where we send our output; wraps clientSocket's outputStream
	private ObjectInputStream in = null; // Where we receive our input; wraps clientSocket's inputStream
	private Object inputObj = null; 
	/**
	 * Constructor that creates a default object of this class
	 */
	public NetworkPlayer(Rules rules, Driver driver, String pName, Color pColor) {
		playerName = pName;
		playerColor = pColor;
		theRules = rules;
		theDriver = driver;
		playerType = 2;
	}

	/**
	 * Set the host that we'll connect to if we're a remote system
	 */
	public void setHost(URL host) {
		this.host = host;
	}

	/**
	 * The host waits for the network player to connect. If there is a timeout,
	 * we generate an actionEvent telling the GUI.
	 * 
	 * @pre this is a network game
	 * @post the connection has been established
	 */
	public void waitForConnect() {

		// fire off an action even to tell the GUI to display the waiting for
		// client dialogue
		// Start up a ServerSocket on port PORTNUM.
		try {
			serverSocket = new ServerSocket(PORTNUM);
		} catch (IOException e) {

			// Should be an actionEvent
			System.err.println("Could not listen on port " + PORTNUM
					+ ".  Exception: \n" + e + "\n\nProgram exiting...");
			cleanup();
			// Questionable
			// theDriver.endGame();
		}

		// Set a timeout
		boolean eThrown = false;
		for (int i = 0; eThrown && i < 4; i++) {
			try {
				serverSocket.setSoTimeout(TIMEOUT);
			} catch (SocketException e) {
				System.err.println("Couldn't set timeout for serverSocket:\n"
						+ e);
				eThrown = true;
			}
		}

		eThrown = false;

		try {
			// Wait for a connect request, then initialize clientSocket
			clientSocket = serverSocket.accept();
			// For communication over network
			out = new ObjectOutputStream(clientSocket.getOutputStream());
			in = new ObjectInputStream(clientSocket.getInputStream());
		} catch (InterruptedIOException e) {
			System.err.println("Timed out while witing for client to connect. "
					+ "Program exiting...");
			cleanup();
			eThrown = true;
		} catch (IOException e) {
			System.err.println("Error while waiting for client to connect "
					+ "(not a timeout).  Program exiting...");
			cleanup();
			eThrown = true;
			// Questionable
			// theDriver.endGame();
		}

		// if eThrown == true, something should be done HERE!

		// if a client connects begin setting up the game
		// get the client player's name
		takeName();

		// tell the client our name
		sendName();

		// send the client their color
		sendColor();
	}

	/**
	 * This method establishes a connection to the host
	 * 
	 * @param host
	 *            - the host to connect to
	 * 
	 * @pre the host specified exists
	 * @post connection to the host has been established
	 */
	public void connectToHost() {
		// create the proper sockets and attempt to establish a connection
		try {
			// Connect to host
			clientSocket = new Socket(host.getHost(), PORTNUM);

			// Streams for communication over network
			out = new ObjectOutputStream(clientSocket.getOutputStream());
			in = new ObjectInputStream(clientSocket.getInputStream());
		} catch (IOException e) {

			System.err.println("Couldn't get I/O for connection to: "
					+ host.getHost() + "\n" + e + "\nProgram exiting...");
			cleanup();
			// theDriver.endGame();
		} catch (Exception e) {
			System.err
					.println("An exception occured while attempting to connect"
							+ "to: " + host.getHost() + "\n" + e
							+ "\nProgram exiting...");
			cleanup();
			// theDriver.endGame();
		}

		// once a connection is established, begin setting up the game
		// send the host our name
		sendName();

		// get the host player's name
		takeName();

		// get our color from the host
		takeColor();
	}

	/**
	 * This method takes the name of the other player and stores it
	 * 
	 * 
	 * @pre name is null
	 * @post the name is displayed at the other end of the connection
	 */
	public void takeName() {
		Object inputObj = null;
		Integer outputObj = null;

		// repeat until a string is received and confirmation is sent
		do {
			// read the name
			try {
				inputObj = in.readObject();
			} catch (Exception e) {
				// add better error detect later

				System.err.println("Error reading name from network"
						+ " stream:\n" + e);
			}

			// make sure it's OK
			if (inputObj instanceof String) {

				// DEBUG
				System.out.println("Received name: " + inputObj);

				// Set the other player's name
				theDriver.setPlayerName(2, (String) inputObj);
				System.out.println("Set the name to " + (String) inputObj);
				outputObj = new Integer(ROGER);

				try {
					System.out.println("Sending confirm");

					out.writeObject(outputObj);
					out.flush();
				} catch (IOException e) {
					System.err.println("IOException while sending confirm"
							+ " over network stream:\n" + e);
				}
			} else {
				outputObj = new Integer(RESEND);
				try {
					out.writeObject(outputObj);
					out.flush();
				} catch (IOException e) {
					System.err.println("IOException while sending confirm"
							+ " over network stream:\n" + e);
				}
			}
		} while (outputObj.intValue() == RESEND);
	}

	/**
	 * This method sends the name of this player to the other computer
	 * 
	 * @pre name is not null
	 * @post the host has the proper name for the player
	 */
	public void sendName() {
		Integer inputObj = null;
		// Keep looping until confirm is received
		do {
			// use the socket's output stream to send our name to the other
			// player
			try {
				// TEMPORARY HACK FOR COMPILE
				// String name = "My Name";
				out.writeObject(playerName);
				out.flush();
				// DEBUG
				System.out.println("Sent name: " + playerName);
			} catch (IOException e) {
				System.err.println("IOException while sending name"
						+ " over network stream:\n" + e);
			}
			// get confirmation from host
			try {
				inputObj = (Integer) in.readObject();
			} catch (Exception e) {
				// add better error detect later
				System.err.println("Error getting confirmation from network"
						+ " stream:\n" + e);
			}
		} while (inputObj.intValue() == RESEND);
	}

	/**
	 * This method gets the color of this player from the other computer; this
	 * method will only be executed by the client computer.
	 * 
	 * @pre the color is null
	 * @post this computer has the proper color for the player
	 */
	public void takeColor() {
		Object inputObj = null;
		Integer outputObj = null;
		// repeat until a Color is received and confirmation is sent
		do {
			// read the Color
			try {
				inputObj = in.readObject();
			} catch (Exception e) {
				// add better error detect later
				System.err.println("Error reading color from network"
						+ " stream:\n" + e);
			}
			// make sure it's OK
			if (inputObj instanceof Color) {
				// DEBUG
				System.out.println("Received color: " + inputObj);
				// Set this player's color
				playerColor = (Color) inputObj;
				// Set the other player's color
				if ( playerType == 1) {
					if (this.playerColor == Color.black) {
						theDriver.setPlayerColor(2, Color.red);
					} else {
						theDriver.setPlayerColor(2, Color.black);
					}
				} else if (this.playerType == 2) {
					if (this.playerColor == Color.black) {
						theDriver.setPlayerColor(1, Color.red);
					} else {
						theDriver.setPlayerColor(1, Color.black);
					}
				}

				outputObj = new Integer(ROGER);
				try {
					out.writeObject(outputObj);
					out.flush();
				} catch (IOException e) {
					System.err.println("IOException while sending confirm"
							+ " over network stream:\n" + e);
				}
			} else {
				outputObj = new Integer(RESEND);
				try {
					out.writeObject(outputObj);
					out.flush();
				} catch (IOException e) {
					System.err.println("IOException while sending confirm"
							+ " over network stream:\n" + e);
				}
			}
		} while (outputObj.intValue() == RESEND);
	}

	/**
	 * This method sends the color of this player to the other computer; will
	 * only be executed by the host computer.
	 * 
	 * @pre the color is not null
	 * @post the other computer has the proper color for the player
	 */
	public void sendColor() {
		Integer inputObj = null;
		// Keep looping until confirm is received
		do {
			// use the socket's output stream to send the color to the client
			try {
				// TEMPORARY HACK FOR COMPILE
				Color color = Color.red;
				out.writeObject(color);
				out.flush();
				System.out.println("Sent color: " + color);
			} catch (IOException e) {
				System.err.println("IOException while sending color"
						+ " over network stream:\n" + e);
			}
			// get confirmation from host
			try {
				inputObj = (Integer) in.readObject();
			} catch (Exception e) {
				// add better error detect later
				System.err.println("Error getting confirmation from network"
						+ " stream:\n" + e);
			}
		} while (inputObj.intValue() == RESEND);
	}

	/**
	 * Wait for the other player to send us a move or command. If there is a
	 * timeout, we generate an actionEvent telling the GUI. We must then send
	 * the move along to Rules, or call processCommand.
	 * 
	 * @pre the game is in progress
	 * @post we have recieved a move from the other player.
	 */
	public void waitForPlayer() {
		// DEBUG
		System.out.println("Entered waitForPlayer.");

		Integer outputObj = new Integer(ROGER);

		// We'll loop until we get good input
		do {
			// Receive something from the remote player
			try {
				inputObj = in.readObject();
				System.out.println("in waitForPlayer:  Read incoming object.");
			} catch (Exception e) {
				// add better error detect later
				System.err.println("Error reading command from network"
						+ " stream:\n" + e);
			}
			if (inputObj instanceof Integer) {
				// If it's an Integer, it's a command, so process it.
				outputObj = processCommand(((Integer) inputObj).intValue());
				try {
					out.writeObject(outputObj);
				} catch (IOException e) {
					System.err.println("IOException while sending confirm"
							+ " over network stream:\n" + e);
				}

				// this may conflict with Driver class
				if (outputObj.intValue() == ACCEPTDRAW) {
					cleanup();
				}
			} else if (inputObj instanceof NetworkMove) {

				NetworkMove remoteMove = (NetworkMove) inputObj;

				// If it's a move, send it off to be validated
				theRules.validateMove(new Move(this,
						remoteMove.startLocation(), remoteMove.endLocation()));

				// DEBUG:
				System.out.println("Move received.");
				outputObj = new Integer(ROGER);
				try {
					out.writeObject(outputObj);
				} catch (IOException e) {
					System.err.println("IOException while sending confirm"
							+ " over network stream:\n" + e);
				}
			} else {
				// The command recieved was bad; ask them to resend it
				outputObj = new Integer(RESEND);
				// DEBUG
				System.err.println("Bad input received over network.");
				try {
					out.writeObject(outputObj);
				} catch (IOException e) {
					System.err.println("IOException while sending confirm"
							+ " over network stream:\n" + e);
				}
			}
		} while (outputObj.intValue() == RESEND);
	}

	/**
	 * Process an incoming command, and take the appropriate action.
	 * 
	 */
	public Integer processCommand(int command) {
		Integer result = new Integer(ROGER);
		int answer = 0;
		System.out.println("Entered processCommand.");

		// If a draw offer is recieved, fire off an action event to inform the
		// GUI.
		if (command == DRAWOFFER) {
			answer = JOptionPane.showConfirmDialog(null, "The remote player"
					+ " has requested a draw." + "\n\nWill you agree to a"
					+ " draw?", "Draw offer", JOptionPane.YES_NO_OPTION);

			// Make the answer readable by the remote player object
			if (answer == JOptionPane.YES_OPTION) {
				result = new Integer(ACCEPTDRAW);
			} else {
				result = new Integer(REFUSEDRAW);
			}

			// Locally, this answer will be returned and sent by waitForPlayer()
		}

		// If a draw accept is recieved, fire off an action event to
		// inform the GUI and call endGame in theDriver.
		else if (command == ACCEPTDRAW) {
			JOptionPane.showMessageDialog(null,
					"The remote player has accepted your draw offer."
							+ "\n\nClick OK to end the program.",
					"Draw offer accepted", JOptionPane.INFORMATION_MESSAGE);
			// theDriver.endGame();
		}

		// If a draw is refused, let the user know and proceed with the game.
		else if (command == REFUSEDRAW) {
			JOptionPane.showMessageDialog(null,
					"The remote player has refused your draw offer."
							+ "\n\nClick OK to continue the game.",
					"Draw offer refused", JOptionPane.INFORMATION_MESSAGE);
		}

		// If a resign command is received, inform the user and exit the game
		else if (command == RESIGN) {
			JOptionPane.showMessageDialog(null,
					"The remote player has resigned."
							+ "\n\nClick OK to end the program.",
					"Remote resignation", JOptionPane.WARNING_MESSAGE);
			cleanup();
			// theDriver.endGame();
		}

		// If an end of game command is received, wait for a text message to
		// be sent over and then fire off an action event to tell the GUI
		// to display the message in a dialogue box when the user clicks OK,
		// endGame should be called in theDriver.
		else if (command == ENDOFGAME) {
			String endMessage = null;
			try {
				endMessage = (String) in.readObject();
			} catch (Exception e) {
				// add better error detect later
				System.err.println("Error reading end of game message from"
						+ " network stream:\n" + e);
			}
			JOptionPane.showMessageDialog(null, endMessage
					+ "n\nClick OK to end the program.", "End of game",
					JOptionPane.INFORMATION_MESSAGE);

			cleanup();
		} else {

			System.err.println("Bad command received over network stream."
					+ "  Unrecoverable error.  Program exiting...");
			// theDriver.endGame();
		}
		return result;
	}

	/**
	 * This method sends some command over network to the local system
	 * 
	 * @param type
	 *            - the type of command that is an integer value
	 * 
	 * @pre type has to be a valid number for the command
	 * @post command is sent to the local user
	 */
	public void sendCommand(int type) {
		Integer inputObj = null;
		// try {
		do {
			// send the command given across the active socket
			try {
				out.writeObject(new Integer(type));
			} catch (IOException e) {
				System.err.println("IOException while sending command over"
						+ " network stream:\n" + e);
			}
			// get the confirmation
			try {
				inputObj = (Integer) in.readObject();
			} catch (Exception e) {
				// add better error detect later
				System.err.println("Error reading confirm from network"
						+ " stream:\n" + e);
			}
		} while (inputObj != null && inputObj.intValue() == RESEND);
		/*
		 * } catch ( IllegalCastException e ) { System.err.println(
		 * "Tried to send a command that wasn't an int." +
		 * "  Program exiting..." ); }
		 */
	}

	/**
	 * The move is sent to the remote player
	 * 
	 * @param move
	 *            - move that was made by the local player
	 * 
	 * @pre move is a legal move
	 * @post the move is sent to the network player
	 */
	public void sendMove() {
		Integer inputObj = new Integer(ROGER);
		do {
			// send the given move across the active socket
			try {
				out.writeObject(new NetworkMove(theMove));

				// DEBUG
				System.out.println("Sent move.");
			} catch (IOException e) {
				System.err.println("IOException while sending move over"
						+ " network stream: " + e);
			}

			// get the confirmation
			try {
				inputObj = (Integer) in.readObject();
			} catch (Exception e) {
				// add better error detect later
				System.err.println("Error reading confirm from network"
						+ " stream.");
			}
		} while (inputObj.intValue() == RESEND);
	}

	/**
	 * A DRAWOFFER message is sent to the remote player. On this end, we will
	 * wait for a reply and generate an appropriate actionEvent based on it.
	 * 
	 */
	public void offerDraw(Player player) {

		// send the draw offer over the network
		try {
			out.writeObject(new Integer(DRAWOFFER));

			// DEBUG
			System.out.println("Sent DRAWOFFER");

		} catch (IOException e) {
			System.err.println("IOException while sending draw offer over"
					+ " network stream.");
		}

		// get the answer; this is handled by waitForPlayer()
		// If the offer is accepted, the player will be informed, and the game
		// will end.
		// If the offer is refuesed, again the player will be informed, and the
		// game will continue as usual.
		waitForPlayer();
	}

	/**
	 * When the current player accepts a draw, this method is called in the
	 * opposite player to inform them that the draw has been accepted. We're a
	 * networkPlayer, so we send the ACCEPTDRAW command over to the remote
	 * player and waitForPlayer.
	 * 
	 */
	public void acceptDraw(Player player) {
		// send the draw accept over the network
		try {
			out.writeObject(new Integer(ACCEPTDRAW));
		} catch (IOException e) {
			System.err.println("IOException while sending accept draw"
					+ " over network stream.");
		}
		JOptionPane.showMessageDialog(null,
				"Since you agreed to a draw, the game" + " is now over."
						+ "\n\nClick OK to end the program.", "End of game",
				JOptionPane.WARNING_MESSAGE);

		cleanup();
		// theDriver.endGame();
	}

	/**
	 * Method that is invoked when the end of game conditions have been met.
	 * Send the ENDOFGAME command over to the remote player and then send the
	 * message endMessage over.
	 * 
	 * @param endMessage
	 *            Message indicating the end of the game.
	 */
	public void endOfGame(String endMessage) {
		try {
			out.writeObject(new Integer(ENDOFGAME));
			out.writeObject(endMessage);
			cleanup();
		} catch (IOException e) {
			System.err.println("IOException while sending end of game"
					+ " message over network stream.");
		}
	}

	/**
	 * Return the integer constant representing the networkPlayer class
	 * 
	 * @pre instance of player exists
	 * @post this method has changed nothing
	 */
	public int getPlayerType() {
		// return the constant representing this class
		return NETWORKPLAYER;
	}

	/**
	 * Closes the streams & sockets
	 */
	public void cleanup() {

		// DEBUG
		System.out.println("Attempting to run cleanup.");

		if (out != null) {
			try {
				out.close();
			} catch (IOException e) {
				System.err.println("Error:  " + playerName
						+ " couldn't close output stream.\n" + e);
			}
		}

		if (in != null) {
			try {
				in.close();
			} catch (IOException e) {
				System.err.println("Error:  " + playerName
						+ " couldn't close input stream.\n" + e);
			}
		}

		if (clientSocket != null) {
			try {
				clientSocket.close();
			} catch (IOException e) {
				System.err.println("Error:  " + playerName
						+ " couldn't close clientSocket.\n" + e);
			}
		}

		if (serverSocket != null) {
			try {
				serverSocket.close();
			} catch (IOException e) {
				System.err.println("Error:  " + playerName
						+ " couldn't close clientSocket.\n" + e);
			}
		}

		System.out.println("Finished cleanup.");
	}

	// BELOW: TEMP METHODS FOR COMPILE

	public void endInDeclineDraw(Player player) {
	}

	public void endInDraw(Player player) {
	}

}
